Fix some warnings
[adiumx.git] / Plugins / Gaim Service / ESGaimJabberAccount.m
blob1bc84f073a5d0564a2c6dccd08b90a1d86df5683
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "ESGaimJabberAccount.h"
18 #import "SLGaimCocoaAdapter.h"
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIStatusControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIChat.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIStatus.h>
27 #import <Adium/ESFileTransfer.h>
28 #import <Adium/ESTextAndButtonsWindowController.h>
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #include <Libgaim/buddy.h>
31 #include <Libgaim/presence.h>
32 #include <Libgaim/si.h>
34 #define DEFAULT_JABBER_HOST @"@jabber.org"
36 extern void jabber_roster_request(JabberStream *js);
38 @implementation ESGaimJabberAccount
39         
40 /*!
41  * @brief The UID will be changed. The account has a chance to perform modifications
42  *
43  * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
44  * name@server.org
45  *
46  * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
47  *
48  * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
49  * @result The UID to use; the default implementation just returns proposedUID.
50  */
51 - (NSString *)accountWillSetUID:(NSString *)proposedUID
53         proposedUID = [proposedUID lowercaseString];
54         NSString        *correctUID;
55         
56         if ((proposedUID && ([proposedUID length] > 0)) && 
57            ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
58                 
59                 NSString        *host;
60                 //Upgrade code: grab a previously specified Jabber host
61                 if ((host = [self preferenceForKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS ignoreInheritedValues:YES])) {
62                         //Determine our new, full UID
63                         correctUID = [NSString stringWithFormat:@"%@@%@",proposedUID, host];
65                         //Clear the preference and then set the UID so we don't perform this upgrade again
66                         [self setPreference:nil forKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS];
67                         [self setPreference:correctUID forKey:@"FormattedUID" group:GROUP_ACCOUNT_STATUS];
69                 } else {
70                         //Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
71                         correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
72                 }
73         } else {
74                 correctUID = proposedUID;
75         }
77         return correctUID;
80 - (const char*)protocolPlugin
82    return "prpl-jabber";
85 - (NSSet *)supportedPropertyKeys
87         static NSMutableSet *supportedPropertyKeys = nil;
88         
89         if (!supportedPropertyKeys) {
90                 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
91                         @"AvailableMessage",
92                         @"Invisible",
93                         nil];
94                 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
95         }
96         
97         return supportedPropertyKeys;
100 - (void)configureGaimAccount
102         [super configureGaimAccount];
103         
104         NSString        *connectServer;
105         BOOL            forceOldSSL, allowPlaintext;
107         gaim_account_set_username(account, [self gaimAccountName]);
109         //'Connect via' server (nil by default)
110         connectServer = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
111         
112         gaim_account_set_string(account, "connect_server", (connectServer ?
113                                                                                                                 [connectServer UTF8String] :
114                                                                                                                 ""));
115         
116         //Force old SSL usage? (off by default)
117         forceOldSSL = [[self preferenceForKey:KEY_JABBER_FORCE_OLD_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
118         gaim_account_set_bool(account, "old_ssl", forceOldSSL);
120         //Allow plaintext authorization over an unencrypted connection? Gaim will prompt if this is NO and is needed.
121         allowPlaintext = [[self preferenceForKey:KEY_JABBER_ALLOW_PLAINTEXT group:GROUP_ACCOUNT_STATUS] boolValue];
122         gaim_account_set_bool(account, "auth_plain_in_clear", allowPlaintext);
125 - (NSString *)serverSuffix
127         AILog(@"using jabber");
128         return DEFAULT_JABBER_HOST;
131 /*!     @brief  Obtain the resource name for this Jabber account.
133  *      This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
135  *      @return The resource name for the account.
136  */
137 - (NSString *)resourceName
139         return [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
142 - (const char *)gaimAccountName
144         NSString        *userNameWithHost = nil, *completeUserName = nil;
145         BOOL            serverAppendedToUID;
146         
147         /*
148          * Gaim stores the username in the format username@server/resource.  We need to pass it a username in this format
149          *
150          * The user should put the username in username@server format, which is common for Jabber. If the user does
151          * not specify the server, use jabber.org.
152          */
153         
154         serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
155         
156         if (serverAppendedToUID) {
157                 userNameWithHost = UID;
158         } else {
159                 userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
160         }
162         completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
164         return [completeUserName UTF8String];
168  * @brief Connect Host
170  * Convenience method for retrieving the connect host for this account
172  * Rather than having a separate server field, Jabber uses the servername after the user name.
173  * username@server.org
175  * The connect server, stored in KEY_JABBER_CONNECT_SERVER, overrides this to provide the connect host. It will
176  * not be set in most cases.
177  */
178 - (NSString *)host
180         NSString        *host;
181         
182         if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
183                 int location = [UID rangeOfString:@"@"].location;
185                 if ((location != NSNotFound) && (location + 1 < [UID length])) {
186                         host = [UID substringFromIndex:(location + 1)];
188                 } else {
189                         host = [self serverSuffix];
190                 }
191         }
192         
193         return host;
197  * @brief Should set aliases serverside?
199  * Jabber supports serverside aliases.
200  */
201 - (BOOL)shouldSetAliasesServerside
203         return YES;
207  * @brief Supports offline messaging?
209  * Jabber supports offline messaging.
210  */
211 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
213         return YES;
216 - (AIListContact *)contactWithUID:(NSString *)sourceUID
218         AIListContact   *contact;
219         
220         contact = [[adium contactController] existingContactWithService:service
221                                                                                                                         account:self
222                                                                                                                                 UID:sourceUID];
223         if (!contact) {         
224                 contact = [[adium contactController] contactWithService:[self _serviceForUID:sourceUID]
225                                                                                                                 account:self
226                                                                                                                         UID:sourceUID];
227         }
228         
229         return contact;
232 - (AIService *)_serviceForUID:(NSString *)contactUID
234         AIService       *contactService;
235         NSString        *contactServiceID = nil;
237         if ([contactUID hasSuffix:@"@gmail.com"] ||
238                 [contactUID hasSuffix:@"@googlemail.com"]) {
239                 contactServiceID = @"libgaim-jabber-gtalk";
241         } else if([contactUID hasSuffix:@"@livejournal.com"]){
242                 contactServiceID = @"libgaim-jabber-livejournal";
243                 
244         } else {
245                 contactServiceID = @"libgaim-Jabber";
246         }
248         contactService = [[adium accountController] serviceWithUniqueID:contactServiceID];
249         
250         return contactService;
253 #pragma mark Status
255 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
257         static AIHTMLDecoder *jabberHtmlEncoder = nil;
258         if (!jabberHtmlEncoder) {
259                 jabberHtmlEncoder = [[AIHTMLDecoder alloc] init];
260                 [jabberHtmlEncoder setIncludesHeaders:NO];
261                 [jabberHtmlEncoder setIncludesFontTags:YES];
262                 [jabberHtmlEncoder setClosesFontTags:YES];
263                 [jabberHtmlEncoder setIncludesStyleTags:YES];
264                 [jabberHtmlEncoder setIncludesColorTags:YES];
265                 [jabberHtmlEncoder setEncodesNonASCII:NO];
266                 [jabberHtmlEncoder setPreservesAllSpaces:NO];
267                 [jabberHtmlEncoder setUsesAttachmentTextEquivalents:YES];
268         }
269         
270         return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
273 - (NSString *)_UIDForAddingObject:(AIListContact *)object
275         NSString        *objectUID = [object UID];
276         NSString        *properUID;
277         
278         if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
279                 properUID = objectUID;
280         } else {
281                 properUID = [NSString stringWithFormat:@"%@@%@",objectUID,[self host]];
282         }
283         
284         return [properUID lowercaseString];
287 - (NSString *)unknownGroupName {
288     return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
291 - (NSString *)connectionStringForStep:(int)step
293         switch (step) {
294                 case 0:
295                         return AILocalizedString(@"Connecting",nil);
296                         break;
297                 case 1:
298                         return AILocalizedString(@"Initializing Stream",nil);
299                         break;
300                 case 2:
301                         return AILocalizedString(@"Reading data",nil);
302                         break;                  
303                 case 3:
304                         return AILocalizedString(@"Authenticating",nil);
305                         break;
306                 case 5:
307                         return AILocalizedString(@"Initializing Stream",nil);
308                         break;
309                 case 6:
310                         return AILocalizedString(@"Authenticating",nil);
311                         break;
312         }
313         return nil;
316 - (BOOL)shouldRequestRosterOnConnect
318         return YES;
321 - (void)accountConnectionConnected
323         //HACK UNTIL LIBGAIM (broken as of [18051]) IS FIXED
324         if ([self shouldRequestRosterOnConnect]) {
325                 JabberStream *js = account->gc->proto_data;
326                 
327                 jabber_roster_request(js);
328         }
330         [super accountConnectionConnected];
333 - (BOOL)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
335         BOOL shouldReconnect = YES;
336         
337         if (disconnectionError && *disconnectionError) {
338                 if (([*disconnectionError rangeOfString:@"401"].location != NSNotFound) ||
339                         ([*disconnectionError rangeOfString:@"Authentication Failure"].location != NSNotFound) ||
340                         ([*disconnectionError rangeOfString:@"Not Authorized"].location != NSNotFound)) {
341                         shouldReconnect = NO;
343                         /* Automatic registration attempt */
344                         //Display no error message
345                         [*disconnectionError release];
346                         *disconnectionError = nil;
348                         [[adium interfaceController] displayQuestion:AILocalizedString(@"Would you like to register a new Jabber account?", nil)
349                                                                                  withDescription:AILocalizedString(@"Jabber was unable to connect due to an invalid Jabber ID or password.  This may be because you do not yet have an account on this Jabber server.  Would you like to register now?",nil)
350                                                                                  withWindowTitle:AILocalizedString(@"Invalid Jabber ID or Password",nil)
351                                                                                    defaultButton:AILocalizedString(@"Register",nil)
352                                                                                  alternateButton:AILocalizedString(@"Cancel",nil)
353                                                                                          otherButton:nil
354                                                                                                   target:self
355                                                                                                 selector:@selector(answeredShouldReigsterNewJabberAccount:userInfo:)
356                                                                                                 userInfo:nil];
358                 } else if ([*disconnectionError rangeOfString:@"Stream Error"].location != NSNotFound) {
359                         shouldReconnect = NO;
361                 } else if ([*disconnectionError rangeOfString:@"requires plaintext authentication over an unencrypted stream"].location != NSNotFound) {
362                         shouldReconnect = NO;
363                         
364                 } else if ([*disconnectionError rangeOfString:@"Resource Conflict"].location != NSNotFound) {
365                         shouldReconnect = NO;
366                 }
367         }
368         
369         return shouldReconnect;
372 - (BOOL)answeredShouldReigsterNewJabberAccount:(NSNumber *)returnCodeNumber userInfo:(id)userInfo
374         AITextAndButtonsReturnCode returnCode = [returnCodeNumber intValue];
376         switch (returnCode) {
377                 case AITextAndButtonsDefaultReturn:
378                         [self performSelector:@selector(performRegisterWithPassword:)
379                                            withObject:password
380                                            afterDelay:1];
381                         break;
383                 case AITextAndButtonsAlternateReturn:
384                 case AITextAndButtonsOtherReturn:
385                 case AITextAndButtonsClosedWithoutResponse:
386                         [self serverReportedInvalidPassword];
387                         break;
388         }
389         
390         return YES;
393 - (void)disconnectFromDroppedNetworkConnection
395         /* Before we disconnect from a dropped network connection, set gc->disconnect_timeout to a non-0 value.
396          * This will let the prpl know that we are disconnecting with no backing ssl connection and that therefore
397          * the ssl connection is has should not be messaged in the process of disconnecting.
398          */
399         GaimConnection *gc = gaim_account_get_connection(account);
400         if (GAIM_CONNECTION_IS_VALID(gc) &&
401                 !gc->disconnect_timeout) {
402                 gc->disconnect_timeout = -1;
403                 AILog(@"%@: Disconnecting from a dropped network connection", self);
404         }
406         [super disconnectFromDroppedNetworkConnection];
409 #pragma mark File transfer
410 - (BOOL)canSendFolders
412         return NO;
415 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
417         [super _beginSendOfFileTransfer:fileTransfer];
420 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
422     [super acceptFileTransferRequest:fileTransfer];    
425 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
427     [super rejectFileReceiveRequest:fileTransfer];    
430 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
432         [super cancelFileTransfer:fileTransfer];
435 #pragma mark Status Messages
436 - (NSAttributedString *)statusMessageForGaimBuddy:(GaimBuddy *)b
438         NSAttributedString  *statusMessage = nil;
440         if (gaim_account_is_connected(account)) {               
441                 char    *normalized = g_strdup(gaim_normalize(b->account, b->name));
442                 JabberBuddy     *jb;
443                 
444                 if ((jb = jabber_buddy_find(account->gc->proto_data, normalized, FALSE))) {
445                         NSString        *statusMessageString = nil;
446                         const char      *msg = jabber_buddy_get_status_msg(jb);
447                         
448                         if (msg) {
449                                 //Get the custom jabber status message if one is set
450                                 statusMessageString = [NSString stringWithUTF8String:msg];
451                         }
452                         
453                         if (statusMessageString && [statusMessageString length]) {
454                                 statusMessage = [AIHTMLDecoder decodeHTML:statusMessageString];
455                         }
456                 }
457                 
458                 g_free(normalized);
459         }
460         
461         return statusMessage;
464 - (NSString *)statusNameForGaimBuddy:(GaimBuddy *)buddy
466         NSString                *statusName = nil;
467         GaimPresence    *presence = gaim_buddy_get_presence(buddy);
468         GaimStatus              *status = gaim_presence_get_active_status(presence);
469         const char              *gaimStatusID = gaim_status_get_id(status);
470         
471         if (!gaimStatusID) return nil;
473         if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
474                 statusName = STATUS_NAME_FREE_FOR_CHAT;
475                 
476         } else if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
477                 statusName = STATUS_NAME_EXTENDED_AWAY;
478                 
479         } else if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
480                 statusName = STATUS_NAME_DND;
481                 
482         }
483         
484         return statusName;
488  * @brief Jabber status messages are plaintext
489  */
490 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
492         return [[inAttributedString attributedStringByConvertingLinksToStrings] string];
495 #pragma mark Menu items
496 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
498         if (strcmp(label, "Un-hide From") == 0) {
499                 return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),[inContact formattedUID]];
501         } else if (strcmp(label, "Temporarily Hide From") == 0) {
502                 return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),[inContact formattedUID]];
504         } else if (strcmp(label, "Unsubscribe") == 0) {
505                 return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),[inContact formattedUID]];
507         } else if (strcmp(label, "(Re-)Request authorization") == 0) {
508                 return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),[inContact formattedUID]];
510         } else if (strcmp(label,  "Cancel Presence Notification") == 0) {
511                 return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),[inContact formattedUID]];       
512         }
513         
514         return [super titleForContactMenuLabel:label forContact:inContact];
517 #pragma mark Multiuser chat
519 //Multiuser chats come in with just the contact's name as contactName, but we want to actually do it right.
520 - (void)addUser:(NSString *)contactName toChat:(AIChat *)chat newArrival:(NSNumber *)newArrival
522         if (chat) {
523                 NSString                *chatNameWithServer = [chat name];
524                 NSString                *chatParticipantName = [NSString stringWithFormat:@"%@/%@",chatNameWithServer,contactName];
525                 AIListContact   *listContact = [self contactWithUID:chatParticipantName];
527                 [listContact setStatusObject:contactName forKey:@"FormattedUID" notify:YES];
528                 
529                 [chat addParticipatingListObject:listContact notify:(newArrival && [newArrival boolValue])];
531                 GaimDebug (@"Jabber: added user %@ to chat %@",chatParticipantName,chatNameWithServer);
532         }       
535 - (oneway void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
537         if (chat) {
538                 NSString        *chatNameWithServer = [chat name];
539                 NSString        *chatParticipantName = [NSString stringWithFormat:@"%@/%@",chatNameWithServer,contactName];
540                 
541                 AIListContact *contact = [self contactWithUID:chatParticipantName];
542                 
543                 [chat removeParticipatingListObject:contact];
544                 
545                 GaimDebug (@"Jabber: removed user %@ to chat %@",chatParticipantName,chatNameWithServer);
546         }       
549 #pragma mark Status
551  * @brief Return the gaim status type to be used for a status
553  * Most subclasses should override this method; these generic values may be appropriate for others.
555  * Active services provided nonlocalized status names.  An AIStatus is passed to this method along with a pointer
556  * to the status message.  This method should handle any status whose statusNname this service set as well as any statusName
557  * defined in  AIStatusController.h (which will correspond to the services handled by Adium by default).
558  * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
559  * [statusState statusType] for a general idea of the status's type.
561  * @param statusState The status for which to find the gaim status ID
562  * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
564  * @result The gaim status ID
565  */
566 - (const char *)gaimStatusIDForStatus:(AIStatus *)statusState
567                                                         arguments:(NSMutableDictionary *)arguments
569         const char              *statusID = NULL;
570         NSString                *statusName = [statusState statusName];
571         NSString                *statusMessageString = [statusState statusMessageString];
572         NSNumber                *priority = nil;
573         
574         if (!statusMessageString) statusMessageString = @"";
576         switch ([statusState statusType]) {
577                 case AIAvailableStatusType:
578                 {
579                         if (([statusName isEqualToString:STATUS_NAME_FREE_FOR_CHAT]) ||
580                            ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_FREE_FOR_CHAT]] == NSOrderedSame))
581                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT);
582                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AVAILABLE group:GROUP_ACCOUNT_STATUS];
583                         break;
584                 }
585                         
586                 case AIAwayStatusType:
587                 {
588                         if (([statusName isEqualToString:STATUS_NAME_DND]) ||
589                            ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND]] == NSOrderedSame))
590                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND);
591                         else if (([statusName isEqualToString:STATUS_NAME_EXTENDED_AWAY]) ||
592                                          ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY]] == NSOrderedSame))
593                                 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA);
594                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
595                         break;
596                 }
597                         
598                 case AIInvisibleStatusType:
599                         AILog(@"Warning: Invisibility is not yet supported in libgaim 2.0.0 jabber");
600                         priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
601                         statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY);
602 //                      statusID = "Invisible";
603                         break;
604                         
605                 case AIOfflineStatusType:
606                         break;
607         }
609         //Set our priority, which is actually set along with the status...Default is 0.
610         [arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
611                                   forKey:@"priority"];
613         //If we didn't get a gaim status ID, request one from super
614         if (statusID == NULL) statusID = [super gaimStatusIDForStatus:statusState arguments:arguments];
615         
616         return statusID;
619 #pragma mark Account Action Menu Items
620 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
622         /* XXX All Jabber account actions depend upon adiumGaimRequestFields */
623         return nil;
626 @end